jwt

您所在的位置:网站首页 golang jwt token jwt

jwt

2023-03-18 15:16| 来源: 网络整理| 查看: 265

GolangでJWT(JSON Web Token)を取り扱いたいと思い、jwt-goの使い方を調べました。

JWTについての説明は他の有益な解説サイトがあるので、そちらを参考にして下さい。

早速実装の説明に入ります。

Routing定義

main.goに、ルーティングの定義をします。この記事では、julienschmidt/httprouterを使用しています。

main.go package main import ( "fmt" "html/template" "log" "net/http" "sync" "github.com/julienschmidt/httprouter" ) type templateHandler struct { once sync.Once filename string templ *template.Template } type API struct { router *httprouter.Router } func main() { fmt.Println("running...") api := API{ router: httprouter.New(), } api.setAppRouter() log.Fatal(http.ListenAndServe(":8080", api.router)) } func (api *API) setAppRouter() { api.router.POST("/tokenAuth", LoginHandler) //Login api.router.GET("/tokenAuthenticate", RequireTokenAuthenticationHandler) //Tokenが有効か確認 }

tokenAuth でJWTの発行、tokenAuthenticateで発行されたJWTが有効か確認します。

Handlerの定義

説明をしやすくするため、handler.goの中身を三分割して説明を行います。

import、変数宣言 handler.go(import、変数宣言) package main import ( "crypto/rsa" "fmt" "io/ioutil" "net/http" "time" jwt "github.com/dgrijalva/jwt-go" request "github.com/dgrijalva/jwt-go/request" "github.com/julienschmidt/httprouter" ) var ( verifyKey *rsa.PublicKey signKey *rsa.PrivateKey )

jwt-goは、version2からversion3に変わった際、JWTを読み込むParseFromRequestメソッドが、requestフォルダに移動されました。 そのため、

jwt "github.com/dgrijalva/jwt-go" request "github.com/dgrijalva/jwt-go/request"

の様に別々importしています。

今回、RSA256を利用し、JWT発行時は秘密鍵でサインを行い、JWT認証時は公開鍵で認証を行います。 そのため、verifyKey, signKeyを宣言しました。

プロジェクトパス以下に秘密鍵、公開鍵を作成します。

$ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key: ${projectのパス}/demo.rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in ${projectのパス}/demo.rsa. Your public key has been saved in ${projectのパス}/demo.rsa.pub. The key fingerprint is: SHA256:8nz3BBJQKWrivQ8Msa/UT92R/JgQTKqKeLWrf7kVXHg @Mac.local The key's randomart image is: +---[RSA 2048]----+ | ..o. | | .*. | | . .o.E | | .ooo o + . | | .++o S o = | | . o.B.+ o + * | |. o + =o= o = o | | . . o+= . . o | | .o+..oo . | +----[SHA256]-----+

公開鍵のフォーマットをpksc8に変更します。 こちらの記載にある通り、公開鍵を読み込むにはpksc1もしくはpksc8でないといけない為です。

$ssh-keygen -f demo.rsa.pub -e -m pkcs8 > demo.rsa.pub.pkcs8 LoginHandler(JWTの発行) handler.go(LoginHandler) // LoginHandler : JWTの発行 func LoginHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) { //今回、username,password共にtestを使っていますが、皆様が実際に行う場合はbodyから読み込んだり、DBから読み込んだりして下さい。 username := "test" password := "test" signBytes, err := ioutil.ReadFile("./demo.rsa") if err != nil { panic(err) } signKey, err := jwt.ParseRSAPrivateKeyFromPEM(signBytes) if err != nil { panic(err) } if username == "test" && password == "test" { // create token token := jwt.New(jwt.SigningMethodRS256) // set claims claims := token.Claims.(jwt.MapClaims) claims["name"] = "test" claims["admin"] = true claims["exp"] = time.Now().Add(time.Hour * 72).Unix() tokenString, err := token.SignedString(signKey) if err != nil { fmt.Println(err) } w.WriteHeader(http.StatusOK) w.Write([]byte(tokenString)) } }

秘密鍵は、読み込んで[]byte型になったsignBytesをParseRSAPrivateKeyFromPEMで*PrivateKey型のsignKeyに格納しています。

claimsの箇所もversion2からvarsion3に変更になった際に仕様が変更になりました。こちらを参照

version2のときは、

version2 token := jwt.New(jwt.SigningMethodRS256) token.Claims["name"] = "test" token.Claims["admin"] = true token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()

と書いていました。

RequireTokenAuthenticationHandler(JWTの認証) handler.go(RequireTokenAuthenticationHandler) // RequireTokenAuthenticationHandler : Tokenの検証 func RequireTokenAuthenticationHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) { verifyBytes, err := ioutil.ReadFile("./demo.rsa.pub.pkcs8") if err != nil { panic(err) } verifyKey, err := jwt.ParseRSAPublicKeyFromPEM(verifyBytes) if err != nil { panic(err) } token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) { _, err := token.Method.(*jwt.SigningMethodRSA) if !err { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } else { return verifyKey, nil } }) if err == nil && token.Valid { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusUnauthorized) } }

公開鍵については、秘密鍵と似た方法で、読み込んで[]byte型になったverifyBytesをParseRSAPublicKeyFromPEMで*PublicKey型のverifyKeyに格納しています。

認証時にややこしい箇所は、下記のParseFromRequestだと思います。

version3 token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) { ...

version2からversion3になった際、ParseFromRequestメソッドに渡す引数が増えました。それが、今回でいうと、request.AuthorizationHeaderExtractor です。

こちらの通り、http.request内のどこにJWTがあるのかを指定することになりました。

Added new interface type Extractor, which is used for extracting JWT strings from http requests. Used with ParseFromRequest and ParseFromRequestWithClaims.

version2の際は、下記の様な書き方でOKでした。

version2 token, err := ParseFromRequest(r, func(token *jwt.Token) (interface{}, error) { ...

httpリクエストでJWTを送る場合、リクエストヘッダのAuthorizationを用いることが多いと思うので、AuthorizationHeaderExtractorを今回使用しています。

それでは早速動作確認を行ってみます。

下記の様なテストを書きました。

handler_test.go(TestLoginHandler) package main import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/julienschmidt/httprouter" ) func TestLoginHandler(t *testing.T) { router := httprouter.New() router.POST("/tokenAuth", LoginHandler) req, err := http.NewRequest("POST", "/tokenAuth", nil) if err != nil { t.Error("NewRequest URI error") } w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != 200 { fmt.Printf("api error. code is %d\n", w.Code) fmt.Printf("api error. header is %#v\n", w.Header()) fmt.Printf("api error. body is %#v\n", w.Body.String()) } else { fmt.Printf("code is %d\n", w.Code) fmt.Printf("header is %#v\n", w.Header()) fmt.Printf("body is %#v\n", w.Body.String()) } }

実行すると、

code is 200 header is http.Header{} body is "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTI3OTY1ODU3LCJuYW1lIjoidGVzdCJ9.a_wC2avD1HhDWypx-xGZo0Vo3Jwe7gpezNGhiSFOm_zR_h1ACcqzFLbYhl1YDgJ2ijTcZD0GUGknZpBOGpYR_fQKQf8i-TnzmYicwzS8MW8p8_X4MOdIyg2_AHsTGsMr5-vW5d3Qd64HHsn45Dz_-TQRhqr1Ws0R2T1GlnEo9bR3ylPBuI9HhYtosSD8mOTiYZYLqbZnl5rRVu0mGtNNHNuEsvxebCLf0o1xdBN-zUzZdaofiLQYtDL9NTvG0taosM5e74HcDfNK2xvnbEXO-MoXttzMVfZK55H2rLdcl9FchUdAfji7-zfXALwp7CCZVMYqq0tlt9Ltw8H8cfzGNw" PASS ok {プロジェクトパス} 0.018s Success: Tests passed.

と、responsebodyの中にJWTが返ってきました。

このJWTを使って、認証が通るか試してみます。

handler_test.go(TestRequireTokenAuthentication) func TestRequireTokenAuthentication(t *testing.T) { router := httprouter.New() router.GET("/tokenAuthenticate", RequireTokenAuthenticationHandler) req, err := http.NewRequest("GET", "/tokenAuthenticate", nil) if err != nil { t.Error("NewRequest URI error") } w := httptest.NewRecorder() req.Header.Set("Authorization", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTI3OTY1ODU3LCJuYW1lIjoidGVzdCJ9.a_wC2avD1HhDWypx-xGZo0Vo3Jwe7gpezNGhiSFOm_zR_h1ACcqzFLbYhl1YDgJ2ijTcZD0GUGknZpBOGpYR_fQKQf8i-TnzmYicwzS8MW8p8_X4MOdIyg2_AHsTGsMr5-vW5d3Qd64HHsn45Dz_-TQRhqr1Ws0R2T1GlnEo9bR3ylPBuI9HhYtosSD8mOTiYZYLqbZnl5rRVu0mGtNNHNuEsvxebCLf0o1xdBN-zUzZdaofiLQYtDL9NTvG0taosM5e74HcDfNK2xvnbEXO-MoXttzMVfZK55H2rLdcl9FchUdAfji7-zfXALwp7CCZVMYqq0tlt9Ltw8H8cfzGNw") router.ServeHTTP(w, req) if w.Code != 200 { fmt.Printf("api error. code is %d\n", w.Code) fmt.Printf("api error. header is %#v\n", w.Header()) fmt.Printf("api error. body is %#v\n", w.Body.String()) } else { fmt.Printf("code is %d\n", w.Code) fmt.Printf("header is %#v\n", w.Header()) fmt.Printf("body is %#v\n", w.Body.String()) } }

結果は、

code is 200 header is http.Header{} body is "" PASS ok {プロジェクトパス} 0.017s Success: Tests passed.

と、無事認証が通ったことが確認できました。

(わかりにくい説明箇所もあったと思うので、随時更新予定です。)



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3